package com.sca.point.config;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sca.point.entity.api.BaseResult;
import com.sca.point.entity.enums.ResultEnum;
import com.sca.point.utils.EnvUtil;
import com.sca.point.utils.TraceUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.*;

/**
 * @author liuxianfei
 * @since 2020/11/11 14:07
 */
@Slf4j
@Aspect
@Configuration
public class LogAspect {

    private static final int PRETTY_PRINTER_CHARCNT_THRESHOLD = 1000;
    private static final List<String> INTERESTING_HEADERS = Arrays.asList("accept", "content-type", "x-real-ip", "x-forwarded-for", "host", "authorization");
    private static final String CONTENT_TYPE_SUFFIX_JSON = "/json";

    private final ObjectMapper mapper;
    private final EnvUtil env;
    private final CommonConfig commonConfig;

    @Autowired
    public LogAspect(ObjectMapper mapper, EnvUtil env, CommonConfig commonConfig) {
        this.mapper = mapper;
        this.env = env;
        this.commonConfig = commonConfig;
    }

    @Pointcut("execution(public * com.sca.point.controller..*.*(..))")
    public void webLog() {
    }

    @Pointcut("execution(public " +
        "com.sca.point.entity.api.BaseResult com.sca.point.controller..*.*(..)) && args(..,res)")
    public void validity(BindingResult res) {
    }

    @Before("webLog()")
    public void logBefore() throws IOException {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
        TraceUtil.assignRequestTraceId(request);
        printRequestInfo(request);
    }

    @AfterReturning(returning = "result", pointcut = "webLog()")
    public void logAfter(Object result) throws JsonProcessingException {
        String s = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(result);
        if (s.length() > PRETTY_PRINTER_CHARCNT_THRESHOLD) {
            s = mapper.writeValueAsString(result);
        }
        log.info("**** response-[{}]\n{}", TraceUtil.getCurrentRequestTraceId(), s);
    }

    @Around("webLog()")
    public Object aroundWebLog(ProceedingJoinPoint joinpoint) throws Throwable {
        long startTime = LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli();
        Object result = joinpoint.proceed();
        long endTime = LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli();
        long time = endTime - startTime;
        if (commonConfig.getSlowReqThreshold() != null && time < commonConfig.getSlowReqThreshold()) {
            return result;
        }
        log.info("**** response-[{}], elapseTime:[{}ms]", TraceUtil.getCurrentRequestTraceId(), time);
        return result;
    }

    @Around(value = "validity(res)", argNames = "joinpoint,res")
    public Object aroundLog(ProceedingJoinPoint joinpoint, BindingResult res) throws Throwable {
        Object result;
        if (res.getGlobalError() != null) {
            return new BaseResult<>(ResultEnum.valueOf(res.getGlobalError().getCode()), res.getGlobalError().getDefaultMessage());
        }
        if (res.hasFieldErrors()) {
            List<FieldError> fieldErrors = res.getFieldErrors();
            Iterator<FieldError> iterator = fieldErrors.iterator();
            FieldError fieldError = iterator.next();
            if (fieldError != null) {
                ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();
                if (TraceUtil.getCurrentRequestTraceId() == null) {
                    TraceUtil.assignRequestTraceId(request);
                }
                printRequestInfo(request);
                BaseResult<?> baseResult = new BaseResult<>(ResultEnum.ERR_PARAM);
                if (env.isProd()) {
                    baseResult.setMsg(fieldError.getDefaultMessage());
                } else {
                    baseResult.setMsg(fieldError.getField() + "," + fieldError.getDefaultMessage());
                }
                return baseResult;
            }
        }
        result = joinpoint.proceed();
        return result;
    }

    private static void printRequestInfo(HttpServletRequest request) throws IOException {
        StringBuilder requestInfo = new StringBuilder(PRETTY_PRINTER_CHARCNT_THRESHOLD);
        // 请求行
        String traceId = TraceUtil.getCurrentRequestTraceId();
        requestInfo.append("#### request-[").append(traceId).append("]-[line]\n");
        String reqLine = getRealIp(request) + " " + request.getMethod() + " " + request.getRequestURI();
        requestInfo.append(reqLine).append("\n");
        // 请求头部
        requestInfo.append("#### request-[").append(traceId).append("]-[header]\n");
        Enumeration<String> headerNames = request.getHeaderNames();
        String headerName;
        while (headerNames.hasMoreElements()) {
            headerName = headerNames.nextElement();
            if (INTERESTING_HEADERS.contains(headerName)) {
                requestInfo.append(headerName).append(": ").append(request.getHeader(headerName)).append("\n");
            }
        }
        // 请求数据
        Enumeration<String> paramNames = request.getParameterNames();
        String paramName;
        String[] paramValue;
        StringBuilder reqData = new StringBuilder();
        while (paramNames.hasMoreElements()) {
            paramName = paramNames.nextElement();
            paramValue = request.getParameterValues(paramName);
            if (paramValue.length == 1) {
                reqData.append(paramName).append("=").append(paramValue[0]);
            } else {
                reqData.append(paramName).append("=").append(Arrays.toString(paramValue));
            }
            if (paramNames.hasMoreElements()) {
                reqData.append("&");
            }
        }
        String reqStr = reqData.toString();
        if (!StringUtils.isEmpty(reqStr)) {
            requestInfo.append("#### request-[").append(traceId).append("]-[data]\n");
            requestInfo.append(reqStr).append("\n\n");
        }

        String contentType = request.getHeader("Content-Type");
        boolean postJsonOrXml = !StringUtils.isEmpty(contentType) && (contentType.contains(CONTENT_TYPE_SUFFIX_JSON));
        if (postJsonOrXml) {
            requestInfo.append("#### request-[").append(traceId).append("]-[body]\n");
            requestInfo.append(getRequestBodyAsStr(request)).append("\n\n");
        }
        log.info("\n{}", requestInfo.toString());
    }

    private static String getRequestBodyAsStr(HttpServletRequest request) throws IOException {
        int contentLength = request.getContentLength();
        if (contentLength < 0) {
            return null;
        }
        byte[] buffer = new byte[contentLength];
        for (int i = 0; i < contentLength; ) {
            int readlen = request.getInputStream().read(buffer, i, contentLength - i);
            if (readlen == -1) {
                break;
            }
            i += readlen;
        }
        return new String(buffer, StandardCharsets.UTF_8);
    }

    private static String getRealIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (!StringUtils.isEmpty(ip) && !"unKnown".equalsIgnoreCase(ip)) {
            int index = ip.indexOf(",");
            if (index != -1) {
                return ip.substring(0, index);
            } else {
                return ip;
            }
        }
        return request.getRemoteAddr();
    }

}
